package gui.util;

import gui.util.event.ButtonStateChangedListener;

import java.util.*;
import javax.swing.*;
import javax.swing.plaf.ActionMapUIResource;
import java.awt.event.*;
import java.awt.*;

/**
 * Class extending JCheckBox by third state - undecided. This third state can be
 * set only via API, clicking the button will swith between selected and
 * unselected. The third state is used for folders that has only some of their
 * files selected. <br>
 * This class support two kinds of events. <code>ActionEvent</code> will occur
 * when the button selection state changes. Each <code>State</code> has its
 * selection state defined.<br>
 * <code>ButtonStateChangedEvent</code> occurs when the state of the button
 * changes. This is usually accompanied by <code>ActionEvent</code> when user
 * clicks the button , but setting such state which has the same selection state
 * as the prevoius state will fire <code>ButtonStateChangedEvent</code>, but not
 * <code>ActionEvent</code>.
 * 
 * @see State
 * @see ButtonStateChangedListener
 * 
 * @author Jakub Závěrka
 */
public class ThreestateCheckBox extends JCheckBox
{
	private State currentState = State.NOT_SELECTED;
	
	//listeners for state changes
	private Collection<ButtonStateChangedListener> buttonStateChangedListeners
		= new HashSet<ButtonStateChangedListener>();
	
	// color of the third state
	private final Color thirdStateColor = new Color(127, 127, 127, 255);

	/**
	 * Constructor.
	 * 
	 * @param text
	 *           Text of the button.
	 * @param icon
	 *           Icon of the button.
	 * @param initial
	 *           Initial state (<code>State</code>).
	 */
	public ThreestateCheckBox(String text, Icon icon, State initial){
		super(text, icon);
		// we will add our own mouse listener to listen for mouse clicks
		super.addMouseListener(new ThreestateMouseListener());

		// redefine action map for our own values
		ActionMap map = new ActionMapUIResource();
		map.put("pressed", new ThreestateAction());
		map.put("released", null);
		SwingUtilities.replaceUIActionMap(this, map);

		// set our model
		setModel(new OurButtonModel());
		// set the initial state
		setState(initial);
	}

	public ThreestateCheckBox(String text, State initial){
		this(text, null, initial);
	}

	public ThreestateCheckBox(String text){
		this(text, State.NOT_SELECTED);
	}

	public ThreestateCheckBox(){
		this(null);
	}
	
	/**
	 * Will set the button to the provided state
	 * 
	 * @param state
	 *           <code>State</code> to be set.
	 */
	public void setState(State state){
		//setting the same state will do nothing
		if(currentState != state){
			State oldState = currentState;
			currentState = state;
			//if selection state of the previoud state is different
			if(oldState.isSelected() != currentState.isSelected()){
				model.setSelected(state.isSelected());
				//fire actionPerformed
				fireActionPerformed(new ActionEvent(
							this, ActionEvent.ACTION_PERFORMED, getActionCommand()));
			}
			//repaint the button
			repaint();
			//and fire buttonStateChanged
			fireButtonStateChanged(oldState, currentState);
		}
	}

	/**
	 * Adds a <code>ButtonStateChangedListener</code> to the collection.
	 */
	public void addButtonStateChangedListener(ButtonStateChangedListener e){
		buttonStateChangedListeners.add(e);
	}

	/**
	 * Removes the specified <code>ButtonStateChangedListener</code> from the 
	 * collection.
	 */
	public void removeButtonStateChangedListener(Object o){
		buttonStateChangedListeners.remove(o);
	}
	
	/**
	 * This will notify all <code>ButtonStateChangedListeners</code> that the
	 * <code>State</code> of the button has changed. 
	 * @param oldState The previous state.
	 * @param newState New state of the button.
	 */
	protected void fireButtonStateChanged(State oldState, State newState){
		ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, getActionCommand());
		for(ButtonStateChangedListener listener : buttonStateChangedListeners){
			listener.buttonStateChanged(e, oldState, newState);
		}
	}

	/**
	 * Returns the current state of the button.
	 */
	public State getState(){
		return currentState;
	}

	/**
	 * Overriden method from <code>JCheckBox</code> to support the third state.
	 * Instead of setting the selection state, it sets the <code>State</code> to
	 * the button.
	 * 
	 * @param b
	 *           Sets <code>State.SELECTED</code> when <code>true</code>, or
	 *           <code>State.NOT_SELECTED</code> when <code>false</code>.
	 */
	@Override public void setSelected(boolean b){
		if(b){
			setState(State.SELECTED);
		}
		else{
			setState(State.NOT_SELECTED);
		}
	}

	/**
	 * Overridden method from <code>JComponent</code> to support the third state.
	 * It will pain the button by calling <code>super.paintComponent(g)</code>
	 * and then, if the button is in the <code>State.UNDECIDED</code> state, it
	 * will paint a gray rectangle in the checkbox area. <br>
	 * Note: This implementation will be the same on all Look and Feels
	 * (painting of the rectangle is hard-coded). May require a bit of fine
	 * tuning to make it really Look and Feel-independent.
	 */
	@Override protected void paintComponent(Graphics g){
		super.paintComponent(g);
		if(getState() == State.UNDECIDED){
			g.setColor(thirdStateColor);
			int x = getInsets().left + 2;
			int y = getHeight() / 2 - 4;
			
			g.fillRect(x, y, 9, 9);
		}
	}

	/**
	 * This will set next state of the button. This method is used in clicking
	 * the button and will toggle between <code>State.SELECTED</code> and
	 * <code>State.UNSELECTED</code>. Third state (<code>State.UNDECIDED</code>)
	 * is never set (must be set programatically).
	 */
	public void next(){
		grabFocus();
		if(currentState == State.SELECTED){
			setSelected(false);
		}
		else{
			setSelected(true);
		}
	}
	
	/**
	 * Overriden method from <code>Component</code> to hinder adding other mouse
	 * listeners. This method is made to not work, so this will NOT add a mouse
	 * listener! <br>
	 * This has been made so to stop Swing from adding their own mouse listeners
	 * and thus interfere with setting the button state (<code>State</code>) via 
	 * clicking.
	 */
	@Override public synchronized void addMouseListener(MouseListener l) {};
	
	/**
	 * Extended <code>DefaultButtonModel</code> to support the third state.
	 * 
	 * @author Jakub Závěrka
	 */
	private class OurButtonModel extends DefaultButtonModel
	{
		/**
		 * Default implementation from <code>DefaultButtonModel</code> fires
		 * <code>actionPerformed</code> when called. We do not want the <code>
		 * setPressed</code> method to fire the <code>actionPerformed</code> 
		 * event. This method is copied from the source, except for the 
		 * action firing code.<br>
		 * <code>actionPerfomed</code> is thrown by <code>setState</code> method.
		 */
	    public void setPressed(boolean b) {
	        if((isPressed() == b) || !isEnabled()) {
	            return;
	        }
	        
	        if (b) {
	            stateMask |= PRESSED;
	        } else {
	            stateMask &= ~PRESSED;
	        }
	            
	        fireStateChanged();
	    }  
	}

	/**
	 * Mouse listener for the button. This button is designed to emulate the 
	 * standard mouse behaviour of a checkbox. The only redefined method is
	 * <code>mouseClicked</code>. 
	 * @author Jakub
	 */
	private class ThreestateMouseListener implements MouseListener
	{
		/**
		 * Will set the "pressed" property of the button model to 
		 * <code>true</code>.
		 */
		@Override public void mousePressed(MouseEvent e) {
			model.setPressed(true);
		}
		
		/**
		 * Will set the "pressed" property of the button model to 
		 * <code>false</code>.
		 */
		@Override public void mouseReleased(MouseEvent e) {
			model.setPressed(false);
		}
		
		/**
		 * Will call <code>next()</code> on the ThreestateCheckBox</code>.
		 */
		@Override public void mouseClicked(MouseEvent e){
			next();
		}

		/**
		 * Will set the "rollover" and "armed" property of the button model to
		 * <code>true</code>.
		 */
		@Override public void mouseEntered(MouseEvent e){
			model.setRollover(true);
			model.setArmed(true);
		}

		/**
		 * Will set the "rollover" and "armed" property of the button model to
		 * <code>false</code>.
		 */
		@Override public void mouseExited(MouseEvent e){
			model.setArmed(false);
			model.setRollover(false);
		}
	}

	/**
	 * Implementation of <code>Action</code> interface to support other ways
	 * of changing the checkbox's state other than by mouse.
	 * 
	 * @author Jakub Závěrka
	 */
	private class ThreestateAction extends AbstractAction
	{
		/**
		 * Will call <code>next()</code> on the ThreestateCheckBox</code>.
		 */
		public void actionPerformed(ActionEvent e){
			next();
		}
	}

	/**
	 * Test class.
	 */
	public static class ThreestateCheckBoxTest
	{
		public static void main(String args[]) throws Exception{
			JFrame frame = new JFrame("ThreestateCheckBoxTest");
			frame.getContentPane().setLayout(new GridLayout(0, 1, 5, 5));
			final ThreestateCheckBox swingBox =
						new ThreestateCheckBox("Testing the threestate checkbox");
			swingBox.setMnemonic('T');
			frame.getContentPane().add(swingBox);
			swingBox.setFont(new Font("Arial", 0, 20));
			swingBox.setSize(new Dimension(500, 120));
			frame.getContentPane().add(new JCheckBox("The normal checkbox"));

			/*UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
			final ThreestateCheckBox winBox =
						new ThreestateCheckBox("Testing the threestate checkbox",
									State.SELECTED);
			frame.getContentPane().add(winBox);
			final JCheckBox winNormal = new JCheckBox("The normal checkbox");
			frame.getContentPane().add(winNormal);*/
			JButton b = new JButton("Nastav nerohodnuto");
			b.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent e){
					swingBox.setState(State.UNDECIDED);
					//winBox.setState(State.UNDECIDED);
				}
			});
			frame.getContentPane().add(b); // wait for 3 seconds, then enable all
														// check boxes

			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			frame.pack();
			frame.setVisible(true);
		}
	}
}
